package cn.zyjblogs.server.user.service.impl;

import cn.zyjblogs.server.user.dto.AuthCodeDto;
import cn.zyjblogs.server.user.dto.AuthorizationCodeDto;
import cn.zyjblogs.server.user.dto.AuthorizationDto;
import cn.zyjblogs.server.user.dto.OAuth2AccessTokenDto;
import cn.zyjblogs.server.user.handler.OauthRequestValidator;
import cn.zyjblogs.server.user.handler.OauthRquestHander;
import cn.zyjblogs.server.user.service.AuthService;
import cn.zyjblogs.server.user.vo.OAuth2AccessTokenVo;
import cn.zyjblogs.starter.common.entity.response.HttpCode;
import cn.zyjblogs.starter.common.exception.AuthRuntimeException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.common.exceptions.UnsupportedResponseTypeException;
import org.springframework.security.oauth2.common.exceptions.UserDeniedAuthorizationException;
import org.springframework.security.oauth2.common.util.OAuth2Utils;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.endpoint.DefaultRedirectResolver;
import org.springframework.security.oauth2.provider.endpoint.RedirectResolver;
import org.springframework.security.oauth2.provider.implicit.ImplicitTokenRequest;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.util.*;

import static cn.zyjblogs.server.user.handler.OauthRquestHander.getAuthorizationRequest;

/**
 * @author zhuyijun
 */
@Service
public class AuthServiceImpl implements AuthService {
    private final TokenGranter tokenGranter;
    private final ClientDetailsService clientDetails;
    private final OAuth2RequestFactory oAuth2RequestFactory;
    private final AuthorizationCodeServices authorizationCodeServices;
    private final PasswordEncoder passwordEncoder;
    private final RedirectResolver redirectResolver;
    private final OauthRequestValidator oauthRequestValidator;
    private final Object implicitLock = new Object();

    public AuthServiceImpl(AuthorizationServerEndpointsConfiguration authorizationServerEndpointsConfiguration,
                           ClientDetailsService clientDetails,
                           AuthorizationCodeServices authorizationCodeServices, PasswordEncoder passwordEncoder,
                           OauthRequestValidator oauthRequestValidator) {
        this.tokenGranter = authorizationServerEndpointsConfiguration.getEndpointsConfigurer().getTokenGranter();
        this.clientDetails = clientDetails;
        this.redirectResolver = new DefaultRedirectResolver();
        this.oAuth2RequestFactory = new DefaultOAuth2RequestFactory(clientDetails);
        this.authorizationCodeServices = authorizationCodeServices;
        this.passwordEncoder = passwordEncoder;
        this.oauthRequestValidator = oauthRequestValidator;
    }

    @Value("${security.oauth2.client.client-id}")
    private String clientId;

    @Override
    public OAuth2AccessTokenVo refreshToken(OAuth2AccessTokenDto oAuth2AccessTokenDto) {
        Map<String, String> parameters = new HashMap<>();
        parameters.put("refresh_token", oAuth2AccessTokenDto.getToken());
        parameters.put("grant_type", "refresh_token");
        ClientDetails authenticatedClient = clientDetails.loadClientByClientId(clientId);
        if (authenticatedClient == null) {
            throw new AuthRuntimeException(HttpCode.INTERNAL_SERVER_ERROR, "客户端获取失败");
        }
        try {
            TokenRequest tokenRequest = oAuth2RequestFactory.createTokenRequest(parameters, authenticatedClient);
            OAuth2AccessToken token = tokenGranter.grant(tokenRequest.getGrantType(), tokenRequest);
            return OAuth2AccessTokenVo.TransferToken(token);
        } catch (Exception e) {
            throw new AuthRuntimeException(HttpCode.INTERNAL_SERVER_ERROR, e.getMessage());
        }
    }

    @Override
    public OAuth2AccessTokenVo checkToken(OAuth2AccessTokenDto oAuth2AccessTokenDto) {
        Map<String, String> parameters = new HashMap<>();
        parameters.put("client_id", oAuth2AccessTokenDto.getToken());
        parameters.put("response_type", "code");
        return null;
    }

    /**
     * 获取授权码
     *
     * @param authorizationCodeDto
     * @return
     * @author zhuyijun
     * @date 2022/10/15
     */
    @Override
    public String getAuthorizationCode(AuthorizationCodeDto authorizationCodeDto) {
        String responseType = authorizationCodeDto.getResponse_type();
        String redirect_url = authorizationCodeDto.getRedirect_url();
        if (!StringUtils.hasLength(responseType)) {
            return OauthRquestHander.getUnsuccessfulRedirect(redirect_url, new OAuth2Exception("responseType不能为空"), false);
        }
        Set<String> responseTypes = Set.of(responseType.split(","));
        if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
            return OauthRquestHander.getUnsuccessfulRedirect(redirect_url, new OAuth2Exception("Unsupported response types: " + responseTypes), false);
        }
        ClientDetails clientDetail = this.clientDetails.loadClientByClientId(authorizationCodeDto.getClient_id());
        if (clientDetail == null) {
            return OauthRquestHander.getUnsuccessfulRedirect(redirect_url, new OAuth2Exception("无此客户端"), false);
        }
        if (!passwordEncoder.matches(authorizationCodeDto.getClient_secret(), clientDetail.getClientSecret())) {
            throw new AuthRuntimeException(HttpCode.BAD_REQUEST, "该客户端认证失败");
        }
        String resolvedRedirect = redirectResolver.resolveRedirect(authorizationCodeDto.getRedirect_url(), clientDetail);
        String scope = authorizationCodeDto.getScope();
        Set<String> scopes = StringUtils.hasLength(scope) ? Set.of(scope.split(",")) : new HashSet<>();
        oauthRequestValidator.validateScope(scopes, clientDetail.getScope());
        Map<String, String> parameters = new HashMap<>(16);
        parameters.put(OAuth2Utils.CLIENT_ID, authorizationCodeDto.getClient_id());
        parameters.put("client_secret", authorizationCodeDto.getClient_secret());
        parameters.put(OAuth2Utils.REDIRECT_URI, resolvedRedirect);
        parameters.put(OAuth2Utils.RESPONSE_TYPE, "code");
        parameters.put(OAuth2Utils.SCOPE, authorizationCodeDto.getScope());
        parameters.put(OAuth2Utils.STATE, authorizationCodeDto.getState());
        parameters.put(OAuth2Utils.USER_OAUTH_APPROVAL, authorizationCodeDto.getUser_oauth_approval());
        AuthorizationRequest authorizationRequest = createAuthorizationRequest(parameters, clientDetail);
        oauthRequestValidator.checkForPreApproval(authorizationRequest, clientDetail);
        String flag = parameters.get(OAuth2Utils.USER_OAUTH_APPROVAL);
        boolean isApproved = "true".equalsIgnoreCase(flag);
        authorizationRequest.setApproved(authorizationRequest.isApproved() && isApproved);
        if (!isApproved) {
            return OauthRquestHander.getUnsuccessfulRedirect(authorizationRequest,
                    new UserDeniedAuthorizationException("User denied access"), false);
        }
        if (responseTypes.contains("token")) {
            return getImplicitGrantResponse(authorizationRequest);
        }
        AuthorizationDto authorizationDto = new AuthorizationDto().createAuthorization();
        try {
            return OauthRquestHander.getSuccessfulRedirect(authorizationRequest,
                    generateCode(authorizationRequest, authorizationDto));
        } catch (OAuth2Exception e) {
            return OauthRquestHander.getUnsuccessfulRedirect(authorizationRequest, e, false);
        }
    }

    @Override
    public OAuth2AccessTokenVo getTokenByAuthorizationCode(AuthCodeDto authCodeDto) {
        Map<String, String> parameters = new HashMap<>();
        parameters.put("redirect_uri", authCodeDto.getRedirect_url());
        parameters.put("grant_type", "authorization_code");
        parameters.put("client_id", authCodeDto.getClient_id());
        parameters.put("code", authCodeDto.getCode());
        parameters.put("client_secret", authCodeDto.getClient_secret());
        ClientDetails clientDetail = clientDetails.loadClientByClientId(authCodeDto.getClient_id());
        if (clientDetail == null) {
            throw new AuthRuntimeException(HttpCode.BAD_REQUEST, "该客户端不存在");
        }
        if (!passwordEncoder.matches(authCodeDto.getClient_secret(), clientDetail.getClientSecret())) {
            throw new AuthRuntimeException(HttpCode.BAD_REQUEST, "该客户端认证失败");
        }
        try {
            TokenRequest tokenRequest = oAuth2RequestFactory.createTokenRequest(parameters, clientDetail);
            OAuth2AccessToken token = tokenGranter.grant(tokenRequest.getGrantType(), tokenRequest);
            return OAuth2AccessTokenVo.TransferToken(token);
        } catch (Exception e) {
            throw new AuthRuntimeException(HttpCode.BAD_REQUEST, e.getMessage());
        }
    }


    /**
     * 处理
     *
     * @param authorizationParameters
     * @param clientDetails
     * @return
     */
    public AuthorizationRequest createAuthorizationRequest(Map<String, String> authorizationParameters, ClientDetails clientDetails) {
        return getAuthorizationRequest(authorizationParameters, clientDetails);

    }


    private String getImplicitGrantResponse(AuthorizationRequest authorizationRequest) {
        try {
            TokenRequest tokenRequest = oAuth2RequestFactory.createTokenRequest(authorizationRequest, "implicit");
            OAuth2Request storedOAuth2Request = oAuth2RequestFactory.createOAuth2Request(authorizationRequest);
            OAuth2AccessToken accessToken = getAccessTokenForImplicitGrant(tokenRequest, storedOAuth2Request);
            if (accessToken == null) {
                throw new UnsupportedResponseTypeException("Unsupported response type: token");
            }
            return OauthRquestHander.appendAccessToken(authorizationRequest, accessToken);
        } catch (OAuth2Exception e) {
            return OauthRquestHander.getUnsuccessfulRedirect(authorizationRequest, e, true);
        }
    }

    private OAuth2AccessToken getAccessTokenForImplicitGrant(TokenRequest tokenRequest,
                                                             OAuth2Request storedOAuth2Request) {
        OAuth2AccessToken accessToken = null;
        // These 1 method calls have to be atomic, otherwise the ImplicitGrantService can have a race condition where
        // one thread removes the token request before another has a chance to redeem it.
        synchronized (this.implicitLock) {
            accessToken = tokenGranter.grant("implicit",
                    new ImplicitTokenRequest(tokenRequest, storedOAuth2Request));
        }
        return accessToken;
    }

    private String generateCode(AuthorizationRequest authorizationRequest, Authentication authentication) throws AuthenticationException {
        try {
            OAuth2Request storedOAuth2Request = this.oAuth2RequestFactory.createOAuth2Request(authorizationRequest);
            OAuth2Authentication combinedAuth = new OAuth2Authentication(storedOAuth2Request, authentication);
            return this.authorizationCodeServices.createAuthorizationCode(combinedAuth);
        } catch (OAuth2Exception e) {
            if (authorizationRequest.getState() != null) {
                e.addAdditionalInformation("state", authorizationRequest.getState());
            }
            throw e;
        }
    }

}
